
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.xssf.usermodel.XSSFRow;
import org.apache.poi.xssf.usermodel.XSSFSheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.regex.Pattern;

/**
 * 写出实体类信息到Excel文件.</br>
 * 依赖：poi-3.16.jar、poi-ooxml-3.16.jar、poi-ooxml-schemas-3.16.jar、
 * xmlbeans-2.6.0.jar、commons-collections4-4.1.jar
 * @author AL
 * 
 */
public class ExcelBuilder<T> {
	
	/**
	 * 特殊列.<br/>
	 * 特殊列：即使设置了在需要时自动转换单元格格式为数字格式，也不会被转换格式的列。
	 * 默认为null。
	 * 注意：列的下标从0开始。
	 */
	private int[] spColumns;
	
	/**
	 * 分隔符.
	 * 默认为英文输入状态下的逗号。
	 * @see ExcelBuilder#setSeparativeSign(String)
	 */
	private String separativeSign = ",";
	
	/**
	 * 标记是否自动转换单元格格式为数字格式（若待填充的文本为数字）.
	 * 默认为true。
	 */
	private boolean transform = true;
	
	/**
	 * 转换单元格格式为数字格式时，数字保留到小数点后的精度.
	 * 默认为2。
	 * @see ExcelBuilder#setScale(int)
	 */
	private int scale = 2;
	
	/**
	 * 匹配数字(非科学计数法).
	 */
	private final Pattern general_num=Pattern.compile("^-{0,1}[0-9]+\\.{0,1}[0-9]*$");
	
	/**
	 * 匹配数字(科学计数法).
	 */
	private final Pattern notation_num=Pattern.compile("^-{0,1}[0-9]+\\.{0,1}[0-9]*[Ee]{1}-{0,1}[0-9]+$");
	
	/**
	 * 等号（英文输入状态）.
	 */
	private final String equal_sign ="=";
	
	/**
	 * 字符串“0”.
	 */
	private final String zero = "0";
	
	/**
	 * 字符串“空”.
	 */
	private final String blank = "空";
	
	/**
	 * 封装了Excel信息的Map.
	 */
	private Map<String,List<String>> map;
	
	/**
	 * 输出的文件名，包含路径（末尾不需要“.xlsx”后缀，且构造方法不会检查路径是否合法，请保证该路径存在并可用）.
	 */
	private String fileName;
	
	public ExcelBuilder() {
		super();
	}
	
	/**
	 * @param map key为Excel中每个分页的名称、value为分页的内容
	 * @param fileName 输出的文件名，包含路径（末尾不需要“.xlsx”后缀，且构造方法不会检查路径是否合法，请保证该路径存在并可用）
	 */
	public ExcelBuilder(Map<String, List<String>> map, String fileName) {
		this.map = map;
		this.fileName = fileName;
	}

	/**
	 * 获取特殊列.<br/>
	 * 特殊列：即使设置了在需要时自动转换单元格格式为数字格式，也不会被转换格式的列。
	 * 注意：列的下标从0开始。
	 * @return
	 */
	public int[] getSpColumns() {
		return spColumns;
	}

	/**
	 * 设置特殊列.<br/>
	 * 特殊列：即使设置了在需要时自动转换单元格格式为数字格式，也不会被转换格式的列。
	 * 若不设置，默认为null。
	 * 注意：列的下标从0开始。
	 * @param spColumns int类型的变长参数
	 */
	public void setSpColumns(int... spColumns) {
		this.spColumns = spColumns;
	}

	/**
	 * 设置分隔符.
	 * 若不设置此项，则默认为英文输入状态下的逗号。
	 */
	public void setSeparativeSign(String separativeSign) {
		if (separativeSign != null && !separativeSign.isEmpty()) {
            this.separativeSign = separativeSign;
        }
	}
	
	/**
	 * 获取分隔符.
	 * @return separativeSign
	 */
	public String getSeparativeSign() {
		return this.separativeSign;
	}
	
	/**
	 * 设置是否自动转换单元格格式为数字格式（若待填充的文本为数字）.
	 * 若不设置此项，则默认自动转换。
	 */
	public void setTransform(boolean transform) {
		this.transform = transform;
	}
	
	/**
	 * 获取是否自动转换单元格格式为数字格式的标记.
	 * @return transform
	 */
	public boolean isTransform() {
		return this.transform;
	}

	/**
	 * 设置转换单元格格式为数字格式时，数字保留到小数点后的精度.
	 * 若不设置此项，则默认为2。
	 */
	public void setScale(int scale) {
		if(scale>-1) {
			this.scale = scale;
		}
	}
	
	/**
	 * 获取转换单元格格式为数字格式时，数字保留到小数点后的精度.
	 * @return scale
	 */
	public int getScale() {
		return this.scale;
	}
	
	/**
	 * 获取封装了的Excel信息.
	 * @return Map&lt;String, List&lt;T&gt;&gt;
	 */
	public Map<String, List<String>> getMap() {
		return map;
	}

	/**
	 * 设置封装了Excel信息的map.
	 * @param map Map&lt;String, List&lt;String&gt;&gt;
	 */
	public void setMap(Map<String, List<String>> map) {
		this.map = map;
	}

	/**
	 * 获取文件名（包含路径）.
	 * @return fileName
	 */
	public String getFileName() {
		return fileName;
	}

	/**
	 * 设置输出的文件名，包含路径（末尾不需要“.xlsx”后缀，且构造方法不会检查路径是否合法，请保证该路径存在并可用）.
	 * @param fileName
	 */
	public void setFileName(String fileName) {
		this.fileName = fileName;
	}

	/**
	 * 写出内容到Excel.
	 * @throws Exception
	 */
    void write() throws Exception {
		// 新建文件输出流及Excel表格缓存区

        try (FileOutputStream out = new FileOutputStream(fileName); Workbook excel = new XSSFWorkbook()) {
            // 遍历键值对，将表格名称和表格内容传递给下一层，用于写出
            for (Entry<String, List<String>> entry : map.entrySet()) {
                // 向Excel（缓存区）内填充内容
                writeWorkbook(entry.getValue(), entry.getKey(), excel);
            }
            // 写出到磁盘文件
            excel.write(out);
        } catch (IOException e) {
            throw new Exception("输出Excel失败", e);
        } catch (StringIndexOutOfBoundsException e) {
            throw new Exception("分隔符不正确导致无法按“=”分割出表头", e);
        } catch (Exception e) {
            throw new Exception("写出内容到Excel遭遇未知异常", e);
        }
		
	}
	
	/**
	 * 向Excel（缓存区）内填充内容.
	 * @param result
	 * @param sheetName
	 * @param excel
	 */
	private void writeWorkbook(List<String> result, String sheetName, Workbook excel) {
		// 若sheetName为空，则填充“空”
		if(sheetName==null  || sheetName.isEmpty()){
			sheetName=blank;
		}
		
		Sheet sheet = excel.createSheet(sheetName);
		
		// 若集合为空，则写出“空”字符
		if (result == null || result.isEmpty()) {
			sheet.createRow(0).createCell(0).setCellValue(blank);
			return;
		}
		
		Row row;
		int size=result.size();
		for (int i = 0; i < size; i++) {
			row = sheet.createRow(i);
            // 将toString返回的字符串按分隔符切割成数组
			String[] infos=result.get(i).split(separativeSign);
			int round=infos.length;
			for (int j = 0; j < round; j++) {
				// 只取“=”之后的字符
				String info =infos[j];

				// 获取单元格
				Cell cell=row.createCell(j);
				
				// 判断是否需要转换单元格格式
                cell.setCellValue(info);
			}
		}
	}
	
	/**
	 * 按下标判断所在列是否属于不需要转换格式的特殊列.
	 * @param index
	 * @return
	 */
	private boolean transform(int index) {
		if (spColumns != null) {
			for (int i : spColumns) {
				if (index == i) {
					return false;
				}
			}
		}
		return true;
	}

    static List<List<String>> readSheet(String fileName, int sheetIndex, int startRow, int endRow, int startCol, int endCol) throws Exception {
        InputStream is = new FileInputStream(fileName);
        XSSFWorkbook hssfWorkbook = new XSSFWorkbook(is);
        XSSFSheet sheet2 = hssfWorkbook.getSheetAt(sheetIndex);
        int listSize = endCol - startCol + 1;
        List<List<String>> sheetList2 = new ArrayList<>(listSize);
        for (int i = startCol; i <= endCol; i++) {
            sheetList2.add(new ArrayList<>());
        }

        for (int rowNum = startRow-1; rowNum < endRow; rowNum++) {
            XSSFRow r = sheet2.getRow(rowNum);
            for (int j = 0; j < sheetList2.size(); j++) {
                if (r.getCell(j) == null || "".equals(r.getCell(j).getStringCellValue())){
                    continue;
                }
                sheetList2.get(j).add(r.getCell(j).getStringCellValue());
            }
        }
        return sheetList2;

    }

}